package gizwits

import spray.json._
import Bytes._

object PropType extends Enumeration {
  val Bool = Value("bool")
  val Array = Value("array")
  val BitSet = Value("BitSet")
  val String = Value("string")
  val Hex = Value("hex")
  val Int = Value("int")
  val UInt8 = Value("UInt8")
  val UInt16 = Value("UInt16")
  val UInt32 = Value("UInt32")
  val P0 = Value("p0")
}

case class ProtoDef(target: String, header: List[Byte], config: Option[Map[String, Any]], commands: Map[Symbol, Command]) {
}

case class Command(cmd: List[Byte], udp: Option[Boolean], props: Option[List[Property]]) {
}

case class NameType(name: Symbol, propType: PropType.Value, dynamic: Option[Boolean])

case class Property(name: Symbol, propType: PropType.Value, dynamic: Option[Boolean], optional: Option[Boolean], subProps: Option[List[NameType]], fixedLength: Option[Int]) {
 def readInt(buffer: java.nio.ByteBuffer, length: Int): Option[(Symbol, Any)] = {
    val isOptional = optional getOrElse false
    if (isOptional && buffer.position == buffer.capacity) None else {
      length match {
        case 1 => Some(name, buffer.get)
        case 2 => Some(name, buffer.getShort)
        case 4 => Some(name, buffer.getInt)
        case _ => None
      }
    }
 }

 def readStr(buffer: java.nio.ByteBuffer, hex: Boolean = false): Option[(Symbol, Any)] = {
    val isDynamic = dynamic getOrElse false
    val isFixedLength = fixedLength getOrElse -1
    val isOptional = optional getOrElse false
    val length = if (isOptional && buffer.position == buffer.capacity) 0 else if (isDynamic) buffer.getShort else if (isFixedLength > 0) isFixedLength else -1
    if (length > 0) {
      val arr = new Array[Byte](if(hex) length/2 else length)
      buffer.get(arr)
      val value = if (hex) bytes2hex(arr.toList) else new String(arr)
      Some(name, value)
    } else None
  }

  def read(buffer: java.nio.ByteBuffer, pos: Int): Option[(Symbol, Any)]= {
    propType match {
      case PropType.String => readStr(buffer)
      case PropType.Hex => readStr(buffer, true)
      case PropType.UInt8 => readInt(buffer, 1)
      case _ => None
    }
  }

  def writeStr(str: String): List[Byte] = {
    val buffer = java.nio.ByteBuffer.allocate(2 + str.length)
    buffer.putShort(str.length.toShort)
    str.getBytes.foreach(buffer.put(_))
    buffer.array.toList
  }

  def writeInt(value: Int, length: Int): List[Byte] = {
    val buffer = java.nio.ByteBuffer.allocate(length)
    length match {
      case 1 => buffer.put(value.toByte)
      case 2 => buffer.putShort(value.toShort)
      case 4 => buffer.putInt(value)
      case _ => return Nil
    }
    buffer.array.toList
  }

  def writeHex(str: String): List[Byte] = {
    val buffer = java.nio.ByteBuffer.allocate(2 + str.length/2)
    buffer.putShort(str.length.toShort)
    hex2bytes(str).foreach(buffer.put(_))
    buffer.array.toList
  }

  def write(value: Any): List[Byte] = {
    propType match {
      case PropType.String => value match {
        case str: String => writeStr(str)
        case _ => Nil
      }
      case PropType.Hex => value match {
        case str: String => writeHex(str)
        case _ => Nil
      }
      case PropType.UInt8 => value match {
        case int: Int => writeInt(int, 1)
        case _ => Nil
      }
      case _ => Nil
    }
  }
}

object ProtoDefJsonProtocol extends DefaultJsonProtocol with NullOptions {
  implicit object AnyJsonFormat extends JsonFormat[Any] {
    def write(x: Any): JsValue = x match {
      case n: Int => JsNumber(n)
      case l: Long => JsNumber(l)
      case d: Double => JsNumber(d)
      case f: Float => JsNumber(f.toDouble)
      case s: String => JsString(s)
      case x: Seq[_] => seqFormat[Any].write(x)
      case m: Map[_, _] if m.isEmpty => JsObject(Map[String, JsValue]())
      // Get the type of map keys from the first key, translate the rest the same way
      case m: Map[_, _] => m.keys.head match {
        case sym: Symbol =>
          val map = m.asInstanceOf[Map[Symbol, _]]
          val pairs = map.map { case (sym, v) => (sym.name -> write(v)) }
          JsObject(pairs)
        case s: String => mapFormat[String, Any].write(m.asInstanceOf[Map[String, Any]])
        case a: Any =>
          val map = m.asInstanceOf[Map[Any, _]]
          val pairs = map.map { case (sym, v) => (sym.toString -> write(v)) }
          JsObject(pairs)
      }
        case a: Array[_] => seqFormat[Any].write(a.toSeq)
        case true        => JsTrue
        case false       => JsFalse
        case p: Product  => seqFormat[Any].write(p.productIterator.toSeq)
        case null        => JsNull
        case x           => JsString(x.toString)
    }
    def read(value: JsValue): Any = {
      value match {
      case JsNumber(n) => n.intValue()
      case JsString(s) => s
      case a: JsArray => listFormat[Any].read(value)
      case o: JsObject => mapFormat[String, Any].read(value)
      case JsTrue => true
      case JsFalse => false
      case x => deserializationError("Do not understand how to deserialize " + x)
    }}
  }
  implicit object ListByteFormat extends JsonFormat[List[Byte]] {
    def write(bytes: List[Byte]): JsValue = {
      JsString(bytes2hex(bytes))
    }
    def read(value: JsValue) = value match {
      case JsString(s) => hex2bytes(s.substring(2))
      case x => deserializationError("Do not understand how to deserialize " + x)
    }
  }
  implicit val propTypeFormat = DataPointJsonProtocol.jsonEnum(PropType)
  implicit val nameTypeFormat = jsonFormat(NameType.apply, "name", "type", "dynamic")
  implicit val propFormat = jsonFormat(Property.apply, "name", "type", "dynamic", "optional", "props", "fixed_length")
  implicit val cmdFormat = jsonFormat3(Command.apply)
  implicit val protoDefFormat: RootJsonFormat[ProtoDef] = jsonFormat4(ProtoDef.apply)
}

object ProtoDef {
  def apply(jsonStr: String): ProtoDef = {
    val json = jsonStr.parseJson
    import ProtoDefJsonProtocol._
    val pd = json.convertTo[ProtoDef]
    pd
  }
}
